package com.vzoom.logdesensitization.mybatis;

import com.baomidou.mybatisplus.core.conditions.ISqlSegment;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.segments.MergeSegments;
import com.baomidou.mybatisplus.core.conditions.segments.NormalSegmentList;
import com.baomidou.mybatisplus.core.enums.SqlKeyword;
import com.vzoom.logdesensitization.DesensitizationStrategy;
import com.vzoom.logdesensitization.util.MapTransforms;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.logging.jdbc.BaseJdbcLogger;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.ArrayUtil;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;

import java.sql.Array;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.*;


@Intercepts({@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
        @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
        @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})})
public class MybatisParameterLogInterceptor implements Interceptor, Ordered {

    private Logger log = LoggerFactory.getLogger(BaseJdbcLogger.class);

    private Logger thisLog = LoggerFactory.getLogger(MybatisParameterLogInterceptor.class);

    private static final String EMPTY_STRING = "";

    private final Configuration configuration;
    private final Map<String, DesensitizationStrategy> strategyMap;
    private final Map<String, String> sqlParameter = new HashMap<>();
    private final TypeHandlerRegistry typeHandlerRegistry;

    public MybatisParameterLogInterceptor(Configuration configuration,
                                          Map<String, DesensitizationStrategy> strategyMap,
                                          Map<String, List<String>> sqlParameter) {
        this.configuration = configuration;
        this.strategyMap = strategyMap;
        MapTransforms.reverseMapIgnoreCase(sqlParameter, this.sqlParameter);
        this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        try {
            return invocation.proceed();
        } finally {
            try {
                String sql = this.getParameterValueString(target);
                log.debug("==> Parameters: {}", sql);
            } catch (Exception e) {
                thisLog.error("sql参数脱敏发生错误", e);
            }
        }
    }

    /**
     * org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters(java.sql.PreparedStatement)
     * org.apache.ibatis.logging.jdbc.BaseJdbcLogger#getParameterValueString()
     *
     * @param target
     * @return
     */
    private String getParameterValueString(Object target) {
        StatementHandler statementHandler = (StatementHandler) target;
        BoundSql boundSql = statementHandler.getBoundSql();
        Object parameterObject = boundSql.getParameterObject();
        List<String> dbFieldNameList = null;
        if (parameterObject instanceof MapperMethod.ParamMap) {
            // mybatis plus 兼容处理
            MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) parameterObject;
            if (paramMap.containsKey("ew")) {
                Object ew = paramMap.get("ew");
                if (ew != null) {
                    if (ew instanceof Wrapper) {
                        dbFieldNameList = extractDbFieldName((Wrapper) ew);
                    }
                }
            }
        }
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();

        if (parameterMappings != null) {
            int size = parameterMappings.size();
            if (size > 0) {
                StringBuilder sb = new StringBuilder();
                Object value;
                String propertyName;
                String trueName;
                String realName;
                int ewIndex = 0;
                int dotIndex;
                for (int i = 0; i < size; i++) {
                    ParameterMapping parameterMapping = parameterMappings.get(i);
                    if (parameterMapping.getMode() != ParameterMode.OUT) {
                        propertyName = parameterMapping.getProperty();
                        trueName = null;
                        if (propertyName.startsWith("et")) {
                            dotIndex = propertyName.indexOf('.');
                            trueName = propertyName.substring(dotIndex+1);
                        } else if (propertyName.startsWith("ew")) {
                            if (dbFieldNameList != null) {
                                trueName = dbFieldNameList.get(ewIndex++);
                            }
                        }
                        if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
                            value = boundSql.getAdditionalParameter(propertyName);
                        } else if (parameterObject == null) {
                            value = null;
                        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                            value = parameterObject;
                        } else {
                            MetaObject metaObject = configuration.newMetaObject(parameterObject);
                            value = metaObject.getValue(propertyName);
                        }
                        if (value == null) {
                            sb.append("null,");
                        } else {
                            if (trueName == null) {
                                realName = propertyName;
                            } else {
                                realName = trueName;
                            }
                            String fieldName = realName.toLowerCase();
                            String simpleName = value.getClass().getSimpleName();
                            if (sqlParameter.containsKey(fieldName)) {
                                String strategyName = sqlParameter.get(fieldName);
                                DesensitizationStrategy desensitizationStrategy = strategyMap.get(strategyName);
                                objectValueString(desensitizationStrategy, simpleName, value, sb);
                            } else {
                                sb.append(objectValueString(value));
                            }
                            sb.append("(").append(simpleName).append("), ");
                        }
                    }
                }
                int length = sb.length();
                sb.delete(length - 2, length);
                return sb.toString();
            } else {
                return EMPTY_STRING;
            }
        } else {
            return EMPTY_STRING;
        }
    }

    private void objectValueString(DesensitizationStrategy desensitizationStrategy,
                                   String simpleName, Object value,
                                   StringBuilder sb) {
        if (value instanceof Array) {
            try {
                final Class<?> componentType = value.getClass().getComponentType();
                if (String.class.equals(componentType)) {
                    String[] stringArray = (String[]) value;
                    sb.append('[');
                    for (String str : stringArray) {
                        sb.append(desensitizationStrategy.desensitization(str));
                        sb.append(", ");
                    }
                    int length = sb.length();
                    sb.delete(length - 2, length);
                    sb.append(']');
                } else {
                    sb.append(ArrayUtil.toString(((Array) value).getArray()));
                }
            } catch (SQLException e) {
                sb.append(value);
            }
        } else if ("String".equals(simpleName)) {
            sb.append(desensitizationStrategy.desensitization(value));
        } else {
            sb.append(value);
        }
    }

    private String objectValueString(Object value) {
        if (value instanceof Array) {
            try {
                return ArrayUtil.toString(((Array) value).getArray());
            } catch (SQLException e) {
                return value.toString();
            }
        }
        return value.toString();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }

    private List<String> extractDbFieldName(Wrapper wrapper) {
        String sqlSet = wrapper.getSqlSet();
        List<String> result = null;
        if (sqlSet != null) {
            result = new ArrayList<>();
            int dotIndex, equalIndex;
            int lastDotIndex = 0;
            while (true) {
                equalIndex = sqlSet.indexOf('=', lastDotIndex);
                if (equalIndex < 0) {
                    break;
                }
                result.add(sqlSet.substring(lastDotIndex, equalIndex));
                dotIndex = sqlSet.indexOf(',', lastDotIndex);
                if (dotIndex < 0) {
                    break;
                }
                lastDotIndex = dotIndex + 1;
            }
        }
        MergeSegments expression = wrapper.getExpression();
        if (expression == null) {
            return Collections.emptyList();
        }
        if (result == null) {
            result = new ArrayList<>();
        }
        NormalSegmentList normal = expression.getNormal();
        String lastFiledName = null;
        for (ISqlSegment iSqlSegment : normal) {
            if (iSqlSegment instanceof Wrapper) {
                result.addAll(extractDbFieldName((Wrapper) iSqlSegment));
                continue;
            }
            if (!(iSqlSegment instanceof SqlKeyword)) {
                String sqlSegment = iSqlSegment.getSqlSegment();
                if (sqlSegment.startsWith("(")) {
                    int count = effectCount(sqlSegment, '#');
                    for (int i = 0; i < count; i++) {
                        result.add(lastFiledName);
                    }
                } else if (!sqlSegment.startsWith("#")) {
                    result.add(sqlSegment);
                    lastFiledName = sqlSegment;
                }
            }
        }
        return result;
    }


    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

    private int effectCount(String context, char c) {
        if (context == null) {
            return 0;
        }
        int length = context.length();
        int result = 0;
        for (int i = 0; i < length; i++) {
            char c1 = context.charAt(i);
            if (c1 == c) {
                result++;
            }
        }
        if (result > 1) {
            result -= 1;
        }
        return result;
    }
}
